﻿//
// Copyright (c) 2011 Canyala Innovation AB
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 
// associated documentation files (the "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
// copies of the Software, and to permit persons to whom the Software is furnished to do so, 
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 
// NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH 
// THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 
//

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Serialization;

using Canyala.Lagoon.Expressions;
using Canyala.Lagoon.Contracts;
using Canyala.Lagoon.Collections;

namespace Canyala.Lagoon.Engineering
{
    /// <summary>
    /// Represents an engineering unit
    /// </summary>
    public partial class Unit
    {
        private static Dictionary<string, Unit> unitsByName = new Dictionary<string, Unit>();
        private static Dictionary<string, Unit> unitsBySymbol = new Dictionary<string, Unit>();        

        /// <summary>
        /// The name of the engineering unit. Typically 'meter'.
        /// </summary>
        public string Name { get; private set; }

        /// <summary>
        /// The symbol of the engineering unit. Typically 'm'.
        /// </summary>
        public string Symbol { get; private set; }

        /// <summary>
        /// The dimension of the engineering unit. Typically 'length'
        /// </summary>
        public string Dimension { get; private set; }

        private Bag<string> excludePrefixes = null;
        private Dictionary<string, Conversion> conversions = new Dictionary<string,Conversion>();

        /// <summary>
        /// Wolf Null object
        /// </summary>
        public static Unit Empty = new Unit { Name = string.Empty, Symbol = string.Empty, Dimension = string.Empty };

        /// <summary>
        /// 
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public static Unit FromName(string name)
        {
            if (!unitsByName.Any())
                InitializeUnits();

            Unit unit;
            if (unitsByName.TryGetValue(name, out unit))
                return unit;

            throw new ArgumentException("{0} is not a valid Unit.Name.", name);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="symbol"></param>
        /// <returns></returns>
        public static Unit FromSymbol(string symbol)
        {
            if (!unitsBySymbol.Any())
                InitializeUnits();

            Unit unit;
            if (unitsBySymbol.TryGetValue(symbol, out unit))
                return unit;

            throw new ArgumentException("{0} is not a valid Unit.Symbol", symbol);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="dimension"></param>
        /// <returns></returns>
        public static IEnumerable<Unit> AllOfDimension(string dimension)
        {
            if (!unitsByName.Any())
                InitializeUnits();

            return unitsByName.Values.Where(unit => unit.Dimension == dimension);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<string> AllDimensions()
        {
            if (!unitsByName.Any()) 
                InitializeUnits();

            return unitsByName.Values.Select(unit => unit.Dimension).Distinct();
        }

        internal static Unit Resolve(Unit a, Unit b, Quantity.Operator op)
        {
            return a;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        /// <param name="unit"></param>
        /// <returns></returns>
        public static Quantity Convert(Quantity value, Unit unit)
        {
            #region - Contracts -
            Contract.Requires(unit != null, "unit must not be null");
            #endregion

            Quantity toValue;
            if (Unit.TryConvert(value, unit, out toValue))
                return toValue;

            throw new ArgumentException(String.Format("No conversion to {0} from {1}", unit.Name, value.Unit.Name));
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        /// <param name="unit"></param>
        /// <returns></returns>
        public static Quantity Convert(Quantity value, string unit)
        {
            Quantity toValue;
            if (Unit.TryConvert(value, Unit.FromSymbol(unit), out toValue))
                return toValue;
          
            throw new ArgumentException(String.Format("No conversion to {0} from {1}", Unit.FromSymbol(unit).Name, value.Unit.Name));
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="fromValue"></param>
        /// <param name="toUnit"></param>
        /// <param name="toValue"></param>
        /// <returns></returns>
        public static bool TryConvert(Quantity fromValue, Unit toUnit, out Quantity toValue)
        {
            Conversion conversion = null;
            if (toUnit.conversions.TryGetValue(fromValue.Unit.Name, out conversion))
            {
                Symbols variables = new Symbols();
                variables.Assign(fromValue.Unit.Symbol, fromValue._amount);
                toValue = new Quantity(Interpreter.Evaluate(conversion.Expression, variables), toUnit);
                return true;
            }

            toValue = new Quantity(0);
            return false;
        }

        internal static Unit FromText(string text)
        {
            #region Requires
            Contract.Requires(text != null, "text must not be null");
            Contract.Requires(2 <= text.Length, "text must greater than one character");
            #endregion

            Unit unit;
            if (unitsBySymbol.TryGetValue(text, out unit))
                return unit;

            // Ignore a possible prefix (Two character prefix)
            if (unitsBySymbol.TryGetValue(text.Substring(2), out unit))
                return unit;

            // Ignore a possible prefix (One character prefix)
            if (unitsBySymbol.TryGetValue(text.Substring(1), out unit))
                return unit;

            throw new ArgumentException("{0} could not be parsed as a Unit", text);
        }

        internal static string ToString(Quantity aValue)
        {
            Contract.Requires(aValue != null, "aValue must not be null");

            if (aValue.Unit.excludePrefixes != null && aValue.Unit.excludePrefixes.Contains(aValue.Prefix.Name))
                return string.Format("{0} {1}", aValue._amount, aValue.Unit.Symbol);
            else
                return string.Format("{0} {1}{2}", aValue._amount / aValue.Prefix.Magnitude, aValue.Prefix.Token, aValue.Unit.Symbol);
        }
    }
}
